Test Doubles

The noble art of cheating

Created by Ignasi 'Iggy' Bosch / @ignasibosch

  1. Introduction
  2. Tests
    • Why
    • What
    • How
  3. Doubles
    • What
    • Why
    • When
    • How
  4. Types
    • Dummy
    • Stub
    • Spy
    • Mock
    • Fake

Unit testing

JAVA

NOT About ANY CONCRETE LIBRARY

NOT About HOW TO TEST

NOT About TDD

ONLY ABOUT TEST DOUBLES

::Noble Art of Cheating

Noble - Be humble and responsable *are not for free

Art - Be wise and clever *use it correctly

Cheating - It's about to create lies *you can end up believing your own lies

Common terminology

SUT - System under test
(a.k.a.: Object-under-test, Component Under Test (CUT))

DOC - Depended-on component
(a.k.a.: Collaborator, Depenency)

Why do we test?

Black Box Testing, Functional Testing, Performance Testing, Regression Testing, Smoke Testing, Sanity Testing, Parallel Testing, Recovery Testing, Installation Testing, Compatibility Testing, Configuration Testing, Compliance Testing, Error-Handling Testing, Manual-Support Testing, Inter-Systems Testing, Exploratory Testing, Volume Testing, Scenario Testing, User Interface Testing, System Testing, User Acceptance Testing, Alpha Testing, Beta Testing, White Box Testing, Unit Testing, Static and Dynamic Analysis, Statement Coverage, Decision Coverage, Condition Coverage, Path Coverage, Integration Testing, Bottom-Up Integration Testing, Top-Down Integration Testing, Security Testing, Mutation Testing, Stress Testing, Resilience Testing ...

What are we testing?

· What is needed to be tested
· It's a waste of time to test third party code
· It's crucial to have deterministic tests · On Objects we test behaviours or states or methods or attributes etc.

How are we testing?

· e.g. How should we to test a car and all its components. Should I drive at night to test the headlights?
· Large tests, functional etc are always more expensive and less precise. Use it wisely

“The term Test Double as the generic term for any kind of pretend object used in place of a real object for testing purposes. The name comes from the notion of a Stunt Double in movies.”

(Mocks Aren't Stubs)
Martin Fowler

Why Doubles:

  • Isolate the code under test
  • Speed up test execution
  • Make execution deterministic
  • Simulate special conditions
  • Gain access to hidden information

When Doubles:

  • Performances
    (e.g. The actual object contains slow algorithms and heavy calculation that may impair the test performances)
  • Slow Tests
    (e.g. The actual object make HTTP requests, reboot some system etc)
  • States
    (e.g. Constellation under test happens rarely such as network failure, etc..)
  • Non-deterministic
    (e.g. Components that have interactions with the real-world such as sensors)
  • The actual object does not exist
    (e.g. Another team is working on it and is not yet ready)
  • Indirect inputs
    A DOC does not provide the control point to allow us to exercise the SUT with the necessary indirect inputs
  • Calls
    (e.g. to spy the calls of the SUT to one of its DOCs)
  • Indirect outputs
    Neither the SUT nor its DOCs provide an observation point for the SUT’s indirect output that we need to verify

How (create) Doubles:

  • Manually
    We don’t need to implement the entire interface of the DOC. Instead, we provide only the functionality needed for our particular test. We can even build different Test Doubles for different tests that involve the same DOC

  • Using a library/ies
    Takes an internal cost. Often uses a Reflection classes or a kind of recording all the internal actions and calls etc. Often is a huge cost behind the scenes, adds a value to the programmer but not to the customer, have to evaluate the costs to see if it worth

DUMMY

  • It should never be used by the SUT so they need no “real” implementation

  • It can be something as simple as passing ‘null’ or a void implementation with exceptions to ensure it’s never leveraged

  • Usually they are just used to fill parameter lists


public void testInvoice_addLineItem_DO() {
    final int QUANTITY = 1;

    // Setup
    Product product = new Product("Dummy Product Name", getUniqueNumber());
    Invoice sut = new Invoice( new DummyCustomer() );
    LineItem expItem = new LineItem(sut, product, QUANTITY);

    // Exercise
    sut.addItemQuantity(product, QUANTITY);

    // Verify
    List lineItems = sut.getLineItems();
    assertEquals("number of items", lineItems.size(), 1);
    LineItem actual = (LineItem)lineItems.get(0);
    assertLineItemsEqual("", expItem, actual);
}
                    

public class DummyCustomer implements ICustomer {
    public DummyCustomer() {
        // Real simple; nothing to initialize!
    }
    public int getZone() {
        throw new RuntimeException("This should never be called!");
    }
}
                        

STUB

  • Usually returns a Hard-Coded data

  • Is used to provide indirect inputs to the SUT

  • A Responder injects valid values, while a Saboteur injects errors or exceptions

  • When we want to avoid building a different Hard-Coded Test Stub for each test, we can use a Configurable one

public void testDisplayCurrentTime_AtMidnight() throws Exception {
    // Setup
    TimeProviderTestStub tpStub = new TimeProviderTestStub();
    tpStub.setHours(0);
    tpStub.setMinutes(0);

    // Instantiate SUT
    TimeDisplay sut = new TimeDisplay();

    // Test Double installation
    sut.setTimeProvider(tpStub);

    // Exercise SUT
    String result = sut.getCurrentTimeAsHtmlFragment();

    // Verify outcome
    String expectedTimeString = "<span class=\"tinyBoldText\">Midnight</span>";
    assertEquals("Midnight", expectedTimeString, result);
}
                    

class TimeProviderTestStub implements TimeProvider {
    // Configuration Interface
    public void setHours(int hours) {
        // 0 is midnight; 12 is noon
        myTime.set(Calendar.HOUR_OF_DAY, hours);
    }
    public void setMinutes(int minutes) {
        myTime.set(Calendar.MINUTE, minutes);
    }
    // Interface Used by SUT
    public Calendar getTime() {
        // @return the last time that was set
        return myTime;
    }
}
                        

SPY

  • Kind of decorator which spies all the activity in a class

  • Capture the indirect output calls made to another component by the SUT for later verification

  • May need to provide values to the SUT in response to method calls (as Stub does)


public void testRemoveFlightLogging_recordingTestStub() throws Exception {
    // fixture setup
    FlightDto expectedFlightDto = createAnUnregFlight();
    FlightManagementFacade sut = new FlightManagementFacadeImpl();

    // Test Double setup
    AuditLogSpy logSpy = new AuditLogSpy();
    sut.setAuditLog(logSpy);

    // exercise
    sut.removeFlight(expectedFlightDto.getFlightNumber());

    // verify
    assertFalse("flight still exists after being removed",
                        sut.flightExists( expectedFlightDto.getFlightNumber()));
    assertEquals("number of calls", 1, logSpy.getNumberOfCalls());
    assertEquals("action code", Helper.REMOVE_FLIGHT_ACTION_CODE, logSpy.getActionCode());
    assertEquals("date", helper.getTodaysDateWithoutTime(), logSpy.getDate());
    assertEquals("user", Helper.TEST_USER_NAME, logSpy.getUser());
    assertEquals("detail", expectedFlightDto.getFlightNumber(), logSpy.getDetail());
}
                    

public class AuditLogSpy implements AuditLog {
    // Fields into which we record actual usage information
    private Date date;
    private String user;
    private String actionCode;
    private Object detail;
    private int numberOfCalls = 0;

    // Recording implementation of real AuditLog interface
    public void logMessage(Date date, String user, String actionCode, Object detail) {
        this.date = date;
        this.user = user;
        this.actionCode = actionCode;
        this.detail = detail;
        numberOfCalls++;
    }

    // Retrieval Interface
    public int getNumberOfCalls() {
        return numberOfCalls;
    }
    public Date getDate() {
        return date;
    }
    public String getUser() {
        return user;
    }
    public String getActionCode() {
        return actionCode;
    }
    public Object getDetail() {
        return detail;
    }
}
                        

MOCK

  • They keep track of which of the mock object’s methods are called, with what kind of parameters, and how many times

  • Pre-programmed with expectations of the activity they are expected to receive

  • Are very similar to spies but mocks differ in the setup and the verification phases

  • The assertions are integrated to the mock object

  • A “strict” Mock Object fails the test if the calls are received in a different order than was specified when the Mock Object was programmed. A “lenient” Mock Object tolerates out-of-order calls.


public void testRemoveFlight_JMock() throws Exception {
    // fixture setup
    FlightDto expectedFlightDto = createAnUnregFlight();
    FlightManagementFacade sut = new FlightManagementFacadeImpl();

    // mock configuration
    Mock mockLog = mock(AuditLog.class);
    mockLog.expects(once())
                        .method("logMessage")
                        .with(
                            eq(helper.getTodaysDateWithoutTime()),
                            eq(Helper.TEST_USER_NAME),
                            eq(Helper.REMOVE_FLIGHT_ACTION_CODE),
                            eq(expectedFlightDto.getFlightNumber())
                        );

    // mock installation
    sut.setAuditLog((AuditLog) mockLog.proxy());

    // exercise
    sut.removeFlight(expectedFlightDto.getFlightNumber());

    // verify
    assertFalse("flight still exists after being removed",
                        sut.flightExists( expectedFlightDto.getFlightNumber()));

    // verify() method called automatically by JMock
}
                    

FAKE

  • Have working implementations, but usually take some shortcut which makes them not suitable for production

  • Most Fake Objects are hand-built

  • Typically implements the same functionality as the real DOC but in a much simpler way

  • The most common reason for using it is that the real DOC is not available yet, is too slow, or cannot be used in the test environment because of deleterious side effects

  • Typical examples are Fake Databases (sqlite or in-memory implementations), testing Web Services or Service Layer etc.

public void testReadWrite_inMemory() throws Exception{
    // Setup
    FlightMgmtFacadeImpl sut = new FlightMgmtFacadeImpl();
    sut.setDao(new InMemoryDatabase());

    BigDecimal yyc = sut.createAirport("YYC", "Calgary", "Calgary");
    BigDecimal lax = sut.createAirport("LAX", "LAX Intl", "LA");
    sut.createFlight(yyc, lax);

    // Exercise
    List flights = sut.getFlightsByOriginAirport(yyc);

    // Verify
    assertEquals( "# of flights", 1, flights.size());
    Flight flight = (Flight) flights.get(0);
    assertEquals( "origin", yyc, flight.getOrigin().getCode());
}
                    

public class InMemoryDatabase implements FlightDao{
    private List airports = new Vector();

    public Airport createAirport(String airportCode, String name, String nearbyCity)
                        throws DataException, InvalidArgumentException {
        assertParamtersAreValid( airportCode, name, nearbyCity);
        assertAirportDoesntExist( airportCode);
        Airport result = new Airport(getNextAirportId(),
        airportCode, name, createCity(nearbyCity));
        airports.add(result);

        return result;
    }

    public Airport getAirportByPrimaryKey(BigDecimal airportId)
                        throws DataException, InvalidArgumentException {
        assertAirportNotNull(airportId);
        Airport result = null;
        Iterator i = airports.iterator();
        while (i.hasNext()) {
            Airport airport = (Airport) i.next();
            if (airport.getId().equals(airportId)) {
                return airport;
            }
        }

        throw new DataException("Airport not found:" + airportId);
    }
}
                    

Some Tools:

  • Java: JMock, Junit, Mockito, EasyMock, PowerMock
  • PHP: phpunit, phpspec, mockery, phake, FBMock
  • Python: unittest.mock, nose, pymox, dingus
  • JavaScript : Jasmine, angular.mock, JSMockito, JSMOCK
  • Ruby : Mocha, RSpec, RR, FlexMock

Some Considerations:

  • Focus on what you want to test making short and easy questions
  • If it's too big → make it smaller
  • If it's tricky → make it simpler
  • If it's not possible → change the design
  • Be wise, be noble, be humble. Cheat responsibly
Amazing books:
 
some Cool stuff:

Test Double Patterns
Martin Fowler: Mocks Aren't Stubs
Unit Testing: Mocks, Stubs and Spies
Steve Hostettler: Fakes, Stubs, Dummy, Mocks, Doubles and All That...
Martin Fowler: Eradicating Non-Determinism in Tests
Types of software Testing
Mocks & stubs by Ken Scambler
SymfonyLive London 2014 - Dave Marshall - Mocks Aren't Stubs, Fakes, Dummies or Spies

Thank You

https://ignasibosch.com/talks/test-doubles

me@ignasibosch.com | @ignasibosch